博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用Node.JS,React,Redux和Redux-saga Part3:身份验证构建Retrogames存档
阅读量:2509 次
发布时间:2019-05-11

本文共 63525 字,大约阅读时间需要 211 分钟。

Here we are at the last part of the Retrogames Archive tutorial. We have now a full working app written in JavaScript and it's pretty much what we were looking to achieve.

这里是Retrogames Archive教程的最后一部分。 现在,我们已经有了一个使用JavaScript编写的功能全面的应用程序,这几乎就是我们想要实现的目标。

However, at present all the users have full access to the common operations on the archive: They can view, create and delete games entries.

但是,目前,所有用户都可以完全访问存档中的常规操作:他们可以查看,创建和删除游戏条目。

In this last part of the tutorial we want to limit the operations of creating and deleting a game to allow only Authenticated user to perform them. Plus, we are going to improve the UI with friendly notifications.

在本教程的最后一部分中,我们希望限制创建和删除游戏的操作,以仅允许经过身份验证的用户执行游戏。 另外,我们将通过友好的通知来改进用户界面。

The project is always available on my , the master branch contains the complete code. Don't forget to copy the up-to-date css in your project!

该项目始终在我的上可用,master分支包含完整的代码。 不要忘记在您的项目中复制最新CSS!

( )

The only prerequisite for this very last part of the tutorial is familiarity with JSON web tokens. I take for granted all the other prerequisites described in the previous tutorials, part1 and part2.

本教程最后一部分的唯一先决条件是熟悉JSON Web令牌。 我理所当然地理解了先前教程part1和part2中描述的所有其他先决条件。

NB: Since we are going to edit/update existing files I am going to highlight the changes with multi-line comments:

注意 :由于我们将要编辑/更新现有文件,因此我将使用多行注释突出显示更改:

/*  * The new code starts after this comment... */

( )

( )

That's the final folder structure:

那是最终的文件夹结构:

--app ----models ------game.js ------user.js ----routes ------game.js ------user.js --config ----index.js --client ----dist ------css --------style.css ------fonts --------PressStart2p.ttf ------index.html ------bundle.js ----src ------actions --------filestack.js --------games.js --------auth.js ------components --------About.jsx --------AddGamePanel.jsx --------Archive.jsx --------Contact.jsx --------Form.jsx --------Game.jsx --------GamesListManager.jsx --------Home.jsx --------index.js --------Login.jsx --------Modal.jsx --------Signup.jsx --------Welcome.jsx ------constants --------auth.js --------filestack.js --------games.js ------containers --------AddGameContainer.jsx --------GamesContainer.jsx --------reducers ----------auth.js ----------filestack.js ----------games.js ----------index.js ----------routing.js --------sagas ----------auth.js ----------filestack.js ----------games.js ----------index.js ------index.js ------routes.js ------store.js ------utils --------authWrapper.js --------authentication.js --.babelrc --package.json --server.js --webpack-loaders.js --webpack-paths.js --webpack.config.js --yarn.lock

If you compare this folder structure with the last one you should notice we mostly added files to handle the user authentication on both the server side (new routes and a model) and the client side (new components, reducers, sagas, actions etc.).

如果将此文件夹结构与最后一个文件夹结构进行比较,您应该注意到我们主要在服务器端(新路径和模型)和客户端(新组件,reducer,sagas,action等)上添加了用于处理用户身份验证的文件。 。

( )

Authentication is handled on both parts of your application, the server and the client, so let's start with the server.

身份验证在应用程序的两个部分(服务器和客户端)上都进行处理,因此让我们从服务器开始。

()

We are using JSON web tokens to add a security layer to the app. Users should be able to sign up or login to modify the games archive.

我们正在使用JSON Web令牌向应用程序添加安全层。 用户应该能够注册或登录以修改游戏档案。

First of all, open server.js and add two new routes:

首先,打开server.js并添加两条新路由:

import express from 'express';import bodyParser from 'body-parser';import mongoose from 'mongoose';import morgan from 'morgan';import {
getGames, getGame, postGame, deleteGame } from './app/routes/game';// New routes and middleware to manage the authenticationimport {
signup, login, verifyAuth } from './app/routes/user';const app = express();const port = process.env.PORT || 8080;const options = {
server: {
socketOptions: {
keepAlive: 1, connectTimeoutMS: 30000 } }, replset: {
socketOptions: {
keepAlive: 1, connectTimeoutMS : 30000 } }};mongoose.Promise = global.Promise;mongoose.connect(YOUR_MONGODB_URL', options);const db = mongoose.connection;db.on('error', console.error.bind(console, 'connection error:'));app.use(bodyParser.urlencoded({
extended: true}));app.use(bodyParser.json());app.use(morgan('dev'));app.use(express.static(__dirname + '/client/dist'));app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*"); res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE'); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-access-token"); next();});/* * New routes to handle Authentication */app.post('/auth/login', login);app.post('/auth/signup', signup);app.route('/games') // verifyAuth is the security middleware to check the authentication .post(verifyAuth, postGame) .get(getGames);app.route('/games/:id') .get(getGame) // Again delete requests pass through the security middleware .delete(verifyAuth, deleteGame);app.route("*").get((req, res) => {
res.sendFile('client/dist/index.html', {
root: __dirname });});app.listen(port);console.log(`listening on port ${
port}`);
  • In the server main file we included two new routes in charge of handling the authentication: A POST requests to /auth/signup inserts a new user into the database and return the token while /auth/login checks the credentials and returns a token as well.

    在服务器主文件中,我们包括两个用于处理身份验证的新路由:对/ auth / signup的 POST请求将一个新用户插入数据库并返回令牌,而/ auth / login检查凭据并还返回令牌。
  • In addition, the route for posting a new game and the route for deleting a game are protected by verifyAuth middleware. Whenever the server receives a request to one of those routes, it first retrieves the token from the request header and verify it. Then, if the verification succeed, a call to next runs the function in charge of posting or deleting a game, otherwise the server simply returns a HTTP 403 forbidden status.

    另外,发布新游戏的路线和删除游戏的路线均由verifyAuth中间件保护。 每当服务器收到对这些路由之一的请求时,它首先会从请求标头中检索令牌并进行验证。 然后,如果验证成功,则对next的调用将运行负责发布或删除游戏的功能,否则服务器仅返回HTTP 403禁止状态。

()

We now need to create the user model to store users in the database so we can go very simple: We need e-mail, password and name fields. In particular, the password will be hashed and to do so we let's add bcryptjs, a small library to help hashing passwords:

现在,我们需要创建用户模型以将用户存储在数据库中,这样我们可以变得非常简单:我们需要电子邮件密码名称字段。 特别是,密码将被哈希处理,为此,我们添加bcryptjs ,这是一个帮助哈希密码的小型库:

yarn add bcryptjs

Let's add a new fileuser.js in /app/models and paste the following code:

让我们在/app/models添加一个新文件user.js并粘贴以下代码:

// Require some dependenciesvar mongoose = require('mongoose');var Schema = mongoose.Schema;var bcrypt = require('bcryptjs');// Our schema defines 3 fields, notice email must be uniquevar userSchema = new mongoose.Schema({
email: {
type: String, unique: true, lowercase: true }, password: {
type: String, select: false }, name: String});userSchema.pre('save', function (next) {
var user = this; // before saving a hashed version of the password is created and saved into the db bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(user.password, salt, function (err, hash) {
user.password = hash; next(); }); });});// This utility function comes handy during authenticationuserSchema.methods.comparePwd = function(password, done) {
// Compare the password sent by the user with the one stored in the db bcrypt.compare(password, this.password, (err, isMatch) => {
done(err, isMatch); });};// Export the modelmodule.exports = mongoose.model('User', userSchema);

()

The logic to authenticate the user is also in charge to create the token to be sent as response: To create the token we can define payload with some useful information:

验证用户身份的逻辑还负责创建作为响应发送的令牌:要创建令牌,我们可以使用一些有用的信息定义有效负载:

  1. sub: The subject is the user name. In the client the token can be decoded and the user name shown in the app along with a welcome message.

    sub :主题是用户名。 在客户端中,可以对令牌进行解码,并在应用程序中显示用户名以及欢迎消息。
  2. exp: The expiration date is set to 1 day and to easily set it we are gonna install moment, very useful JavaScript library to work with dates.

    exp :到期日期设置为1天,为了方便地进行设置,我们将安装moment ,这是一个非常有用JavaScript库来处理日期。
jsonwebtoken and
jsonwebtoken
moment:
moment
yarn add jsonwebtoken moment

In server.js we defined two new routes and a middleware all residing in the same file /app/routes/user.js. Let's create it and past the following code:

server.js我们定义了两个新路由和一个中间件,它们都位于同一文件/app/routes/user.js 。 让我们创建它,并通过以下代码:

// Our new dependenciesimport jwt from 'jsonwebtoken';import moment from 'moment';// We import the User model we have just definedimport User from '../models/user';// The config file contains the secret to sign the tokenimport config from '../../config';// Utility function to create and return the token, it requires TOKEN_SECRET from configconst createToken = name => {
var payload = {
sub: name, exp: moment().add(1, 'day').unix() }; return jwt.sign(payload, config.TOKEN_SECRET);}// signup function for the /auth/signup routeconst signup = (req, res) => {
// query the database to make sure the e-mail is not taken already User.findOne({
email: req.body.email }, (err, existingUser) => {
if (existingUser) {
// HTTP 409 status is sent in case the e-mail is taken return res.status(409).json({
message: 'Email is already taken' }); } // A new user is created with the information sent by the client const user = Object.assign(new User(), req.body); user.save((err, result) => {
if (err) {
res.send(err); } // Notice we also send the token as we want the user to be immediately logged in res.json({
message: 'Welcome to Retrogames, you are now logged in', token: createToken(result.name) }); }); });};// Login function for /auth/loginconst login = (req, res) => {
// Query the database for user with that specific e-mail User.findOne({
email: req.body.email }, '+password', (err, user) => {
if (!user) {
// If the user doesn't exist just send a HTTP 401 status return res.status(401).json({
message: 'Invalid email/password' }); } /* If the user exists, the password sent by the client is compared with the one in the db with the utilily function comparePwd */ user.comparePwd(req.body.password, (err, isMatch) => {
if (!isMatch) {
// In case of wrong password, we send another HTTP 401 status return res.status(401).send({
message: 'Invalid email/password' }); } // Correct information from the client, a token is sent res.json({
message: 'You are now logged in', token: createToken(user.name) }); }); });};// verifyAuth middleware to protect post and delete routesconst verifyAuth = (req, res, next) => {
// Get the token from the header x-access-token const token = req.headers['x-access-token']; if (token) {
// Verifies the token and the expiration jwt.verify(token, config.TOKEN_SECRET, function(err, payload) {
// If the verification fails it returns http status 403 if (err) {
return res.status(403).send({
message: 'Failed to authenticate token.' }); } else {
// Goes to the next route since there are no errors next(); } }); } else {
// Requests without token return http status 403 return res.status(403).send({
message: 'No token provided.' }); }};// Export the functions for server.jsexport {
signup, login, verifyAuth};

There are 4 functions we created and 3 of them are exported for further usage in server.js.

我们创建了4个函数,其中3个已导出,以便在server.js进一步使用。

  • createToken: It's an utility function in charge to create and return a valid token.

    createToken :它是一个实用程序函数,负责创建和返回有效令牌。
  • signup: The function receives a new user's information to create a new user entry in the database and returns the token. Before that, it checks if the e-mail was taken by another user before.

    signup :该函数接收新用户的信息以在数据库中创建新的用户条目并返回令牌。 在此之前,它将检查该电子邮件之前是否已被其他用户接收。
  • login: Whenever a user try to authenticate to /auth/login, the function first retrieves the correct user from the database (given the e-mail) and then verify the password. If all goes right, it sends the token back to the client.

    login :每当用户尝试通过/ auth / login进行身份验证时,该函数都会首先从数据库(通过电子邮件)中检索正确的用户,然后验证密码。 如果一切正常,它将令牌发送回客户端。
  • verifyAuth: This is the middleware in charge to protect the archive: Only authenticated user can create or delete games.

    verifyAuth :这是负责保护存档的中间件:只有经过身份验证的用户才能创建或删除游戏。

Take a look at createToken: To sign the token we need a secret string that we imported from a config file, let's create /config/index.js and paste the following code:

看一下createToken :要对令牌进行签名 ,我们需要一个从配置文件中导入的秘密字符串,让我们创建/config/index.js并粘贴以下代码:

const TOKEN_SECRET = process.env.TOKEN_SECRET || 'YOUR_SECRET_STRING';export default {
TOKEN_SECRET};

Replace YOUR_SECRET_STRING with a string of your choice.

用您选择的字符串替换YOUR_SECRET_STRING

The server side is done, let's test it with Postman!

服务器端已完成,让我们用Postman进行测试!

()

The first thing we can do is to try to add a new game with no token in the header to verify whether the server actually refuses your request or not:

我们可以做的第一件事是尝试添加一个新游戏,并且标题中没有令牌,以验证服务器是否实际上拒绝了您的请求:

()

in Postman, send a new game to localhost:8080/games and you should receive a HTTP 403 status with message "No token provided.":

在Postman中,将一个新游戏发送到localhost:8080 / games ,您应该收到HTTP 403状态,并显示消息“未提供令牌”:

The middleware seems to be working fine! Let's create a user now:

中间件似乎运行良好! 现在创建一个用户:

()

We need to send an e-mail, name and password to the server at /auth/signup, let's see if it works:

我们需要通过/ auth / signup向服务器发送电子邮件,名称和密码,让我们看看它是否有效:

As you can see we received a token, let's now use it for creating a new game!

如您所见,我们收到了令牌,现在让我们使用它来创建新游戏!

()

We copy the token in the header tab for the post request. Here is the result:

我们在发布请求的标题标签中复制令牌。 结果如下:

And the game was successfully created!

游戏成功创建!

Finally, we want to simulate the login request.

最后,我们要模拟登录请求。

()

Let's login to the server by sending e-mail and password:

让我们通过发送电子邮件和密码登录服务器:

And this works too, we can now go working on the client-side!

这也可行,我们现在可以在客户端上工作了!

( )

In the client-side of the app we want show the buttons to login, sign-up or logout so let's take a look at these two screenshots:

在应用程序的客户端,我们希望显示用于登录,注册或注销的按钮,因此让我们看一下以下两个屏幕截图:

  1. The user is not authenticated:

    用户未通过身份验证:

  • The users can login or sign-up.

    用户可以登录或注册。
  • Since the user is not authenticated, he/she cannot add/delete any games.

    由于用户未通过身份验证,因此他/她无法添加/删除任何游戏。
  1. The user is authenticated:

    用户已通过身份验证:

  • The user is now authenticated so the buttons to login/sign-up don't show.

    现在,用户已通过身份验证,因此不会显示用于登录/注册的按钮。
  • The user has now the right to add/delete a game. Notice on the top right the welcome message along with the logout button.

    用户现在有权添加/删除游戏。 请注意右上角的欢迎消息以及注销按钮。

Finally, the views to login/sign-up are pretty straightforward:

最后,登录/注册的视图非常简单:

()

Before writing the authentication logic, let's think about the steps to login/signup: Once the user clicks on the login/register button, the client communicates with the server and if all goes well it receives back the token. At this point, a redirection the user to /games should happen. I find react-router-redux package very useful because we can control the routing in the state and we have a handy action-creator push to dispatch actions to the reducer and change view.

在编写身份验证逻辑之前,让我们考虑一下登录/注册的步骤:用户单击“登录/注册”按钮后,客户端与服务器通信,如果一切顺利,它将接收回令牌。 此时,应该将用户重定向到/ games 。 我发现react-router-redux软件包非常有用,因为我们可以控制状态下的路由,并且我们有一个方便的动作创建者push可以将动作分派给reducer并更改视图。

Let's add it by running:

让我们通过运行添加它:

yarn add react-router-redux

To use the push action-creator we need to add a new middleware when configuring the store:

要使用push动作创建者,我们需要在配置商店时添加新的中间件:

In /client/src/store.js paste the following code:

/client/src/store.js粘贴以下代码:

import {
createStore, applyMiddleware, compose} from 'redux';import createSagaMiddleware from 'redux-saga';import rootSaga from './sagas';import reducer from './reducers';/* * Here we imported the routerMiddleware */ import {
routerMiddleware } from 'react-router-redux';import {
hashHistory } from 'react-router';const configureStore = () => {
const sagaMiddleware = createSagaMiddleware(); /* * It requires the app history as parameter */ const routeMiddleware = routerMiddleware(hashHistory); const store = createStore( reducer, /* * we add it right after sagaMiddleware */ applyMiddleware(sagaMiddleware, routeMiddleware) ); sagaMiddleware.run(rootSaga); return store;}export default configureStore;

Since we are using Immutable for the state we need to define the routing reducer by ourselves as well as pass a selector to access the payload state and convert it to a JavaScript object.

由于我们将Immutable用于状态,因此我们需要自己定义路由缩减器,并传递选择器以访问有效负载状态并将其转换为JavaScript对象。

NB: These steps are not necessary with a mutable state.

注意 :对于可变状态,这些步骤不是必需的。

Let's create the reducer in /client/src/reducers/routing.js:

让我们在/client/src/reducers/routing.js创建reducer:

// This is a standard definition for the routing reducerimport Immutable from 'immutable';import {
LOCATION_CHANGE } from 'react-router-redux';const initialState = Immutable.fromJS({
locationBeforeTransitions: null});export default (state = initialState, action) => {
if (action.type === LOCATION_CHANGE) {
return state.set('locationBeforeTransitions', action.payload); } return state;};

This is pretty straightforward, there is just an action to interact with the reducer and it comes directly from react-router-redux. We also gotta edit the index.js in /client/src/reducers:

这非常简单,只有一个动作与reducer交互,它直接来自react-router-redux 。 我们还必须在/client/src/reducers编辑index.js

import {
combineReducers } from 'redux-immutable';import {
reducer as form } from 'redux-form/immutable';import games from './games';import filestack from './filestack';/* * Here we imported the routing reducer */ import routing from './routing';export default combineReducers({
games, form, filestack, /* * Combine routing as well */ routing,});

The last thing to do is to call syncHistoryWithStore to get the routing part of the state and convert it to an object.

最后要做的是调用syncHistoryWithStore以获取状态的路由部分并将其转换为对象。

In /client/src/routes.js paste the following code:

/client/src/routes.js粘贴以下代码:

import React from 'react';import {
Provider } from 'react-redux';import configureStore from './store';import {
Router, Route, hashHistory, IndexRoute } from 'react-router';import {
AddGameContainer, GamesContainer } from './containers';import {
Home, Archive, Welcome, About, Contact } from './components';/* * Here we imported syncHistoryWithStore */ import {
syncHistoryWithStore } from 'react-router-redux';const store = configureStore();/* * Sync navigation events with the store */const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.get('routing').toObject(); }});const routes = (
);export default routes;

That's all, the navigation is now synchronized with the store and we can dispatch actions to change the views. If you are interested in digging more into redux and immutability I suggest you to take a look at documentation where it also further explains what we have just done.

如此,导航现在已与商店同步,我们可以调度动作来更改视图。 如果您有兴趣进一步研究redux和不变性,建议您看一看文档,该文档还将进一步说明我们所做的工作。

Time to write the login.

是时候写登录了。

()

Let's start by the view, create a Login component in /client/src/components/Login.jsx and paste the following code:

让我们从视图开始,在/client/src/components/Login.jsx创建一个Login组件,并粘贴以下代码:

// We import a bunch of dependenciesimport React, {
PureComponent } from 'react';import {
Link } from 'react-router';import {
Field, reduxForm } from 'redux-form/immutable';// Some action-creators we are going to write laterimport * as authActionCreators from '../actions/auth';import {
connect } from 'react-redux';import {
bindActionCreators } from 'redux';class Login extends PureComponent {
login () {
// dispatch action to the redux-saga this.props.authActions.loginUser(this.props.location.query.next || '/games'); } render () {
const {
picture, uploadPicture } = this.props; return (
Back

Login

); }}// Bind the action-creators so that we can call them as propsfunction mapDispatchToProps (dispatch) {
return {
authActions: bindActionCreators(authActionCreators, dispatch) };}// Wrap the login into a reduxForm HOCexport default reduxForm({
form: 'login' })(connect(null, mapDispatchToProps)(Login));

The Login component is nothing but a redux-form asking for e-mail and password (remember the test with Postman?). The login button is responsible for calling the login function which dispatches the action described by loginUser.

登录组件不过是一个要求电子邮件和密码的redux表单(还记得邮递员的测试吗?)。 登录按钮负责调用login功能,该功能分派loginUser描述的操作。

Let's make the Login available through /client/src/components/index.js:

让我们通过/client/src/components/index.js使登录可用:

import About from './About';import Contact from './Contact';import Form from './Form';import Game from './Game';import GamesListManager from './GamesListManager';import Home from './Home';import Archive from './Archive';import Modal from './Modal';import Welcome from './Welcome';/*  * Here we import Login.jsx */ import Login from './Login';export {
About, Contact, Form, Game, GamesListManager, Home, Archive, Modal, Welcome, Login // Export Login};

What about the login route? We wanto to show the login view at localhost:8080/auth/login so let's edit /client/src/routes.js:

那登录路线呢? 我们想在localhost:8080 / auth / login上显示登录视图,所以让我们编辑/client/src/routes.js

import React from 'react';import {
Provider } from 'react-redux';import configureStore from './store';import {
Router, Route, hashHistory, IndexRoute } from 'react-router';import {
AddGameContainer, GamesContainer } from './containers';/* * We can conveniently import Login with all the other components */ import {
Home, Archive, Welcome, About, Contact, Login } from './components';/* * Here we imported syncHistoryWithStore */ import {
syncHistoryWithStore } from 'react-router-redux';const store = configureStore();/* * Sync navigation events with the store */ const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.get('routing').toObject(); }});const routes = (
{
/* * The Archive component defines the layout that works for both Login and Signup */ }
);export default routes;

Notice the new Route with path /auth has component Archive, in fact the layout of the login page (and as we will see later, of the signup too) is very similar to the AddGame view so we can reuse it.

请注意,路径为/ auth的新Route具有组件Archive ,实际上,登录页面(以及我们稍后将看到的注册页面)的布局与AddGame视图非常相似,因此我们可以重复使用它。

We haven't defined the action-creators yet, let's do it.

我们尚未定义动作创建者,让我们开始吧。

In /client/src/actions create a file auth.js and paste the following code:

/client/src/actions创建一个文件auth.js并粘贴以下代码:

// We always define constantsimport {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE,} from '../constants/auth';// Intercepted by a redux-sagafunction loginUser (redirection) {
return {
type: LOGIN_USER, redirection };}// In case of successful response from the serverfunction loginUserSuccess (token) {
// It carries the token! return {
type: LOGIN_USER_SUCCESS, token };}// In case of failurefunction loginUserFailure () {
return {
type: LOGIN_USER_FAILURE };}export {
loginUser, loginUserSuccess, loginUserFailure};
  • The loginUser action-creator dispatch a LOGIN_USER action which is intercepted by a saga we are writing later.

    loginUser操作创建者调度LOGIN_USER操作,该操作被我们稍后编写的传奇拦截。
  • The saga sends user credentials to the server and wait for a token, then call loginUserSuccesful or loginUserFailure according to the response.

    传奇将用户凭据发送到服务器并等待令牌,然后根据响应调用loginUserSuccesfulloginUserFailure

You should be already familiar with the pattern of writing 3 action-creators.

您应该已经熟悉编写3个动作创建者的模式。

Let's now define the constants, create a file auth.js in /src/client/constants:

现在让我们定义常量,在/src/client/constants创建一个文件auth.js

// New constants for the loginconst LOGIN_USER = 'LOGIN_USER';const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';export {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE,};

The code is pretty straightforward, we export the constants to be used throughout the project.

该代码非常简单,我们导出要在整个项目中使用的常量。

Then, let's create the reducer in a new file in /client/src/reducers/auth.js:

然后,让我们在/client/src/reducers/auth.js中的新文件中创建reducer:

import Immutable from 'immutable';// We neeed jwt-decode to take the user name from the token and store it in the stateimport jwtDecode from 'jwt-decode';import {
LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE,} from '../constants/auth';// The initial state has no token hence no name and isAuthenticated is falseconst initialState = Immutable.Map({
isAuthenticated: false, token: null, name: null});export default (state = initialState, action) => {
switch (action.type) {
// Once the server sent a token, the saga dispatches loginUserSuccess case LOGIN_USER_SUCCESS: {
return state.merge({
isAuthenticated: true, token: action.token, name: jwtDecode(action.token).sub }); } // In case of failure the state goes back to the initial one case LOGIN_USER_FAILURE: return state.merge(initialState); default: return state; }}
  • isAuthenticated is a very important field, later we will use to query the state and see whether the user is authenticated or not.

    isAuthenticated是一个非常重要的字段,稍后我们将使用它来查询状态并查看用户是否已通过身份验证。
  • Moreover, we decoded the token to get the user name and include it in the welcome message.

    此外,我们对令牌进行了解码以获取用户名并将其包含在欢迎消息中。

Have you noticed we imported jwt-decode? That's a simple library to help us decode the token in the frontend side of the app. Let's install it:

您是否注意到我们导入了jwt-decode ? 这是一个简单的库,可帮助我们在应用程序前端解码令牌。 让我们安装它:

yarn add jwt-decode

Let's update /client/src/reducers/index.js:

让我们更新/client/src/reducers/index.js

import {
combineReducers } from 'redux-immutable';import {
reducer as form } from 'redux-form/immutable';import games from './games';import filestack from './filestack';/* * Import the auth reducer */ import auth from './auth';import routing from './routing';export default combineReducers({
games, form, filestack, auth, // Combine it with the other reducers routing});

Finally, we need a new saga to communicate with the server. So create auth.js in /client/src/sagas and paste the following code:

最后,我们需要一个新的传奇来与服务器通信。 因此,在/client/src/sagas创建auth.js并粘贴以下代码:

import {
takeLatest } from 'redux-saga';import {
put, call, select } from 'redux-saga/effects';// We import the constant to use it in the watcherimport {
LOGIN_USER } from '../constants/auth';import {
loginUserSuccess, loginUserFailure} from '../actions/auth';// push action-creators to change the viewimport {
push } from 'react-router-redux';// We want to show a notification to the user once logged inimport {
actions as toastrActions} from 'react-redux-toastr';// Selector to get the credential from the formconst getForm = (state, form) => {
return state.getIn(['form', form]).toJS();}// Fetch sends the credentials to the serverconst sendCredentials = (route, credentials) => {
return fetch(`http://localhost:8080/auth/${
route}`, {
headers: new Headers({
'Content-Type': 'application/json' }), method: 'POST', body: JSON.stringify(credentials) }) .then(response => {
if (response.status === 200) {
return response.json(); // This contains the token! } throw response; });};function* loginUser (action) {
// The redirection changes the view to the main page const {
redirection } = action; try {
const credentials = yield select(getForm, 'login'); const result = yield call(sendCredentials, 'login', credentials.values); // Redux-toastr shows the users nice notifications yield put(toastrActions.add({
type: 'success', // success is a green notification title: 'Retrogames Archive', message: result.message })); // We also save the token in the local storage localStorage.setItem('token', result.token); // We send the token to the reducer yield put(loginUserSuccess(result.token)); // Redirect to the main page! yield put(push(redirection)); } catch (e) {
// The status 401 has a personalized message to show in a notification let message = ''; if(e.status === 401) {
message = 'Invalid email/password'; } else {
message = 'Sorry, an error occured!'; } // Set the state to initial state yield put(loginUserFailure()); yield put(toastrActions.add({
type: 'error', // Red notification title: 'Retrogames Archive', message: message })); }}// Saga watcher to intercept LOGIN_USERexport function* watchLoginUser () {
yield takeLatest(LOGIN_USER, loginUser);}

Although the loginUser seems more complicated than the others we wrote in the past, it is actually very easy, the comments in the code highlight the steps. So, you must have notice redux-toastr that, as the name says, implement toastrs notifications to be used with redux.

尽管loginUser似乎比我们过去编写的其他用户更为复杂,但实际上非常简单,代码中的注释突出了步骤。 因此,您必须注意到redux-toastr ,顾名思义,它实现了与redux一起使用的toastrs通知。

We haven't added them yet, so let install it:

我们尚未添加它们,因此请安装它:

yarn add redux-toastr

Redux-toastr requires to add its own reducer, edit /client/src/reducers/index.js:

Redux-toastr需要添加自己的reducer,编辑/client/src/reducers/index.js

import {
combineReducers } from 'redux-immutable';import {
reducer as form } from 'redux-form/immutable';import games from './games';import filestack from './filestack';import auth from './auth';import routing from './routing';/* * Import redux-toastr reducer */ import {
reducer as toastr} from 'react-redux-toastr'export default combineReducers({
games, form, filestack, auth, routing, toastr // Combine toastr});

Then, it requires its own css, edit /client/dist/index.html:

然后,它需要自己CSS,编辑/client/dist/index.html

      
Retrogames Archive

The last step is to add ReduxToastr component at the root of the app, let's edit /client/src/routes.js:

最后一步是在应用程序的根目录添加ReduxToastr组件,让我们编辑/client/src/routes.js

import React from 'react';import {
Provider } from 'react-redux';import configureStore from './store';import {
Router, Route, hashHistory, IndexRoute } from 'react-router';import {
AddGameContainer, GamesContainer } from './containers';import {
Home, Archive, Welcome, About, Contact, Login } from './components';import {
syncHistoryWithStore } from 'react-router-redux';/* * Import ReduxToastr */ import ReduxToastr from 'react-redux-toastr';const store = configureStore();const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.get('routing').toObject(); }});const routes = (
{
/* * We add the div wrapper as Route and ReduxToastr are on the same level */ }
{
/* * we can customize it's behavior and look through props /* }
);export default routes;

For more information regarding Redux-toastr options, check the .

有关Redux-toastr选项的更多信息,请参阅 。

The login logic is done, let's do the signup that you will see it's very similar.

登录逻辑已完成,让我们进行注册,您将看到它非常相似。

()

Let's create Signup.jsx in /client/src/components and paste the following code:

让我们在/client/src/components创建Signup.jsx并粘贴以下代码:

import React, {
PureComponent } from 'react';import {
Link } from 'react-router';import {
Field, reduxForm } from 'redux-form/immutable';import * as authActionCreators from '../actions/auth';import {
connect } from 'react-redux';import {
bindActionCreators } from 'redux';class Signup extends PureComponent {
// signupUser dispatches SIGNUP_USER to be intercepted by a redux-saga register () {
this.props.authActions.signupUser(); } render () {
const {
picture, uploadPicture } = this.props; return (
Back

Sign Up

); }}// Bint the action-creators to be used as propsfunction mapDispatchToProps (dispatch) {
return {
authActions: bindActionCreators(authActionCreators, dispatch) };}// redux-form HOC to wrap the componentexport default reduxForm({
form: 'signup' })(connect(null, mapDispatchToProps)(Signup));

It's very similar to Login so let's move on and edit /client/src/components/index.js to make the component available:

它与Login非常相似,因此让我们继续并编辑/client/src/components/index.js以使该组件可用:

import About from './About';import Contact from './Contact';import Form from './Form';import Game from './Game';import GamesListManager from './GamesListManager';import Home from './Home';import Archive from './Archive';import Modal from './Modal';import Welcome from './Welcome';import Login from './Login';/*  * Import Signup */ import Signup from './Signup';export {
About, Contact, Form, Game, GamesListManager, Home, Archive, Modal, Welcome, Login, Signup // Export Signup};

And then we create its own route at /auth/signup. Let's edit /client/src/routes.js and paste the following:

然后在/ auth / signup创建自己的路由。 让我们编辑/client/src/routes.js并粘贴以下内容:

import React from 'react';import {
Provider } from 'react-redux';import configureStore from './store';import {
Router, Route, hashHistory, IndexRoute } from 'react-router';import {
AddGameContainer, GamesContainer } from './containers';/* * We also import Signup */ import {
Home, Archive, Welcome, About, Contact, Login, Signup } from './components';import {
syncHistoryWithStore } from 'react-router-redux';import ReduxToastr from 'react-redux-toastr';const store = configureStore();const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.get('routing').toObject(); }});const routes = (
{
/* * Signup Route */ }
);export default routes;

To define the action-creators we are going to edit /client/src/actions/auth.js:

为了定义动作创建者,我们将编辑/client/src/actions/auth.js

import {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, /* * New constants to be imported */ SIGNUP_USER, SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE} from '../constants/auth';function loginUser (redirection) {
return {
type: LOGIN_USER, redirection };}function loginUserSuccess (token) {
return {
type: LOGIN_USER_SUCCESS, token };}function loginUserFailure () {
return {
type: LOGIN_USER_FAILURE };}/* * signupUser dispatched from Signup component */ function signupUser () {
return {
type: SIGNUP_USER };}/* * SignupUserSuccess send the token to be added to the state */ function signupUserSuccess (token) {
// It carries the token! return {
type: SIGNUP_USER_SUCCESS, token };}/* * In case of server failure */function signupUserFailure () {
return {
type: SIGNUP_USER_FAILURE };}export {
loginUser, loginUserSuccess, loginUserFailure, signupUser, signupUserSuccess, signupUserFailure};

As we did for login, let's add the constants in /client/src/constants/auth.js:

就像登录一样,让我们​​在/client/src/constants/auth.js添加常量:

const LOGIN_USER = 'LOGIN_USER';const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';/*  * New constants */const SIGNUP_USER = 'SIGNUP_USER';const SIGNUP_USER_SUCCESS = 'SIGNUP_USER_SUCCESS';const SIGNUP_USER_FAILURE = 'SIGNUP_USER_FAILURE';export {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, SIGNUP_USER, SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE};

And now we are going to edit the reducer in /src/clients/reducers/auth.js:

现在,我们将在/src/clients/reducers/auth.js编辑reducer:

import Immutable from 'immutable';import jwtDecode from 'jwt-decode';import {
LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, /* * New constants */ SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE} from '../constants/auth';const initialState = Immutable.Map({
isAuthenticated: false, token: null, name: null});// The actions dispatched by the signup logic do exactly the same of login onesexport default (state = initialState, action) => {
switch (action.type) {
case SIGNUP_USER_SUCCESS: case LOGIN_USER_SUCCESS: {
return state.merge({
isAuthenticated: true, token: action.token, name: jwtDecode(action.token).sub }); } case SIGNUP_USER_FAILURE: // All the failures simply return the initial state case LOGIN_USER_FAILURE: return state.merge(initialState); default: return state; }}

Someone may wonder why we should define different constants for login and signup as they practically do the same: This is actually a personal choice, in my case I want to make it easier to read and understand that the action belongs to the signup logic instead of the login one.

有人可能会奇怪,为什么我们应该为登录和注册定义不同的常量,因为它们实际上是一样的:这实际上是个人选择,在我的情况下,我想更容易阅读和理解该操作属于注册逻辑,而不是登录一个。

Finally, let's edit /client/src/sagas/auth:

最后,让我们编辑/client/src/sagas/auth

import {
takeLatest } from 'redux-saga';import {
put, call, select } from 'redux-saga/effects';import {
LOGIN_USER, SIGNUP_USER } from '../constants/auth';import {
loginUserSuccess, loginUserFailure, /* * We import the signup action-creators */ signupUserSuccess, signupUserFailure} from '../actions/auth';import {
push } from 'react-router-redux';import {
actions as toastrActions} from 'react-redux-toastr';const getForm = (state, form) => {
return state.getIn(['form', form]).toJS();}const sendCredentials = (route, credentials) => {
return fetch(`http://localhost:8080/auth/${
route}`, {
headers: new Headers({
'Content-Type': 'application/json' }), method: 'POST', body: JSON.stringify(credentials) }) .then(response => {
if (response.status === 200) {
return response.json(); } throw response; });};function* loginUser (action) {
const {
redirection } = action; try {
const credentials = yield select(getForm, 'login'); const result = yield call(sendCredentials, 'login', credentials.values); yield put(toastrActions.add({
type: 'success', title: 'Retrogames Archive', message: result.message })); localStorage.setItem('token', result.token); yield put(loginUserSuccess(result.token)); yield put(push(redirection)); } catch (e) {
let message = ''; if(e.status === 401) {
message = 'Invalid email/password'; } else {
message = 'Sorry, an error occured!'; } yield put(loginUserFailure()); yield put(toastrActions.add({
type: 'error', title: 'Retrogames Archive', message: message })); }}/* * the new sagas to handle signup */ function* signupUser () {
try {
// We get the credentials from the form in the state const credentials = yield select(getForm, 'signup'); const result = yield call(sendCredentials, 'signup', credentials.values); // Show a notification in the browser yield put(toastrActions.add({
type: 'success', title: 'Retrogames Archive', message: result.message })); // Set the token in the local storage localStorage.setItem('token', result.token); // Update the state with the token yield put(signupUserSuccess(result.token)); // Redirect to /games yield put(push('/games')); } catch (e) {
// As we did for loginUser, we show a personalized message according to the error status let message = ''; if(e.status === 409) {
message = 'Email is already taken'; } else {
message = 'Sorry, an error occured!'; } // Set the auth portion of the state to the initial value yield put(signupUserFailure()); yield put(toastrActions.add({
type: 'error', title: 'Retrogames Archive', message: message })); }}export function* watchLoginUser () {
yield takeLatest(LOGIN_USER, loginUser);}/* * Signup watcher */export function* watchSignupUser () {
yield takeLatest(SIGNUP_USER, signupUser);}

And we are done for the sign-up logic! We need to run the two sagas watchLoginUser and watchSignupUser. Edit /client/src/sagas/index.js and paste the following code:

我们已经完成了注册逻辑! 我们需要运行两个sagas watchLoginUser和watchSignupUser。 编辑/client/src/sagas/index.js并粘贴以下代码:

import {
watchGetGames, watchDeleteGame, watchPostGame} from './games';import {
watchUploadPicture } from './filestack';/* * The new watchers in charge of the authentication */ import {
watchLoginUser, watchSignupUser } from './auth';export default function* rootSaga () {
yield [ watchGetGames(), watchDeleteGame(), watchPostGame(), watchUploadPicture(), watchLoginUser(), watchSignupUser() ];}

Now you can create a user and login by manually type /auth/signup or /auth/login.However the app has no buttons to login/sign-up, plus users who actually are not authenticated can still create or delete games. It's now time to protect the app and this concludes our tutorial.

现在您可以通过手动输入/ auth / signup/ auth / login来创建用户并登录。但是,该应用程序没有用于登录/注册的按钮,而且未经身份验证的用户仍然可以创建或删除游戏。 现在该保护该应用了,我们的教程到此结束。

()

So we want to protect some routes in our app and possibly hide/show the buttons to delete or add a game. To do so we are going to rely on redux-auth-wrapper. So what is this authentication wrapper? The idea is that we can decouple Authentication and Authorization with high order components that wrap other components, this is kinda useful to protect routes too!

因此,我们希望保护应用程序中的某些路线,并可能隐藏/显示按钮以删除或添加游戏。 为此,我们将依赖redux-auth-wrapper 。 那么这个认证包装器是什么呢? 这个想法是,我们可以将身份验证和授权与包装其他组件的高阶组件分离,这对于保护路由也很有用!

Let's go on and install it:

让我们继续安装它:

yarn add redux-auth-wrapper

if you open /client/src/routes.js you should know that on /games/add the router is going to show AddGameContainer, however this should not happen when the user is not authenticated. Otherwise he/shes is going to be redirected to the login page.

如果打开/client/src/routes.js您应该知道/ games / add上的路由器将显示AddGameContainer ,但是当用户未通过身份验证时不会发生这种情况。 否则,他/她将被重定向到登录页面。

Let's see it in action, we can create authWrapper.js file in /client/src/utils and paste the following code:

让我们来看看它的作用,我们可以在/client/src/utils创建authWrapper.js文件,并粘贴以下代码:

// We import the wrapper component import {
UserAuthWrapper } from 'redux-auth-wrapper';// We export a simple function which receives some options and return the wrapperexport default (options) => UserAuthWrapper(options);

The options we provide are useful to define customized rules for the app.

我们提供的选项对于定义应用程序的自定义规则很有用。

Now in /client/routes.js paste the following code:

现在在/client/routes.js粘贴以下代码:

import React from 'react';import {
Provider } from 'react-redux';import configureStore from './store';import {
Router, Route, browserHistory, hashHistory, IndexRoute } from 'react-router';import {
AddGameContainer, GamesContainer } from './containers';import {
Home, Archive, Welcome, About, Contact, Login, Signup } from './components';import {
UserAuthWrapper } from 'redux-auth-wrapper';import {
push } from 'react-router-redux';import {
syncHistoryWithStore } from 'react-router-redux';import ReduxToastr from 'react-redux-toastr';/* * We imported the utility function */ import userAuthenticated from './utils/authWrapper';const store = configureStore();const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.get('routing').toObject(); }});/* * Here we set the rules for the wrapper */ const options = {
authSelector: state => state.get('auth'), predicate: auth => auth.get('isAuthenticated'), redirectAction: ({
pathname, query }) => {
if(query.redirect) {
// If the user is not logged in go to /auth/login return push(`auth${
pathname}?next=${
query.redirect}`); } }, wrapperDisplayName: 'UserIsJWTAuthenticated'};const requireAuthentication = userAuthenticated(options);const routes = (
{
/* * you can see that AddGameContainer is now literally wrapped */ }
);export default routes;

So what have we actually done?

那么我们实际上做了什么?

  • authSelector receives a selector of part of the state, in our case the auth has it's contain isAuthenticated

    authSelector接收状态的一部分的选择器,在本例中,auth具有它的包含isAuthenticated
  • predicate receives the result from authSelector and according to the boolean balue of isAuthenticated it will either go to games/add or to auth/login.

    谓词从authSelector接收结果,根据isAuthenticated的布尔值,它将进入游戏/添加或进入auth /登录
  • As you can redirectAction is a redux action-creator, we check the redirection and in case push to /auth/login?next=query.redirect. query.redirect is actually /games/add so when the user logged in the view automatically shows the form to add a new game.

    如您所料, redirectAction是一个Redux操作创建者,我们检查重定向,以防推送到/auth/login?next=query.redirectquery.redirect实际上是/ games / add,因此当用户登录视图时,它会自动显示添加新游戏的表单。
  • wrapperDisplayName is a name describing the authorization.

    wrapperDisplayName是描述授权的名称。

It's great, in few lines of code we have now a wrapper we can reuse to protect all the other routes that require authentication. However, we can do more than this, redux-auth-wrapper can actually hide/show any component, which is what we want to achieve: Show/hide the delete game button, or show the add game button when the user is authenticated while replacing it with login and sign-up buttons when not authenticated. Let's see how easy it is!

太好了,在几行代码中,我们现在有了一个包装器,可以重复使用该包装器来保护所有其他需要身份验证的路由。 但是,我们可以做更多的事情, redux-auth-wrapper实际上可以隐藏/显示任何组件,这是我们想要实现的:显示/隐藏“删除游戏”按钮,或者在用户通过身份验证时显示“添加游戏”按钮。未经身份验证时将其替换为登录和注册按钮。 让我们看看它有多简单!

Let's start from the delete button, to hide/show it we simply wrap it and pass the right options! Edit /client/src/components/Game.jsx and paste the following code:

让我们从删除按钮开始,要隐藏/显示它,我们只需要包装它并传递正确的选项即可! 编辑/client/src/components/Game.jsx并粘贴以下代码:

import React, {
PureComponent } from 'react';import {
Link } from 'react-router';/* * We import our utility function */ import userAuthenticated from '../utils/authWrapper';/* * Here we set the rules to hide/show the delete button */ const options = {
authSelector: state => state.get('auth'), predicate: auth => auth.get('isAuthenticated'), wrapperDisplayName: 'authDeleteGame', FailureComponent: null};/* * We define the 'wrapper' version of the delete button */ const DeleteButton = userAuthenticated(options)( (props) => );export default class Game extends PureComponent {
render () {
const {
_id, i, name, description, picture, toggleModal, deleteGame } = this.props; return (
...
{
name}

{

`${
description.substring(0, 150)}...`}

{
/* * the new DeleteButton */ }
); }}
  • As we have done before to protect the rule, we check inside the app state and if isAuthenticated the button will show up in the page.

    正如我们之前为保护规则所做的那样,我们在应用状态内部进行检查,如果已验证为isAuthenticated ,则该按钮将显示在页面中。
  • FailureComponent is just a component we can render in the page when the user is not authorized. In this case it's set to null because there is no need but it comes handy with the sign-up/login panel instead.

    FailureComponent只是当用户未经授权时可以在页面中呈现的组件。 在这种情况下,将其设置为null是因为没有必要,但是它在注册/登录面板中很方便。

Finally, we want to show the add game button when the user is authenticated, plus a welcome message and logout button. But, when the user is not logged in, then show the buttons sign-in and login.

最后,我们希望在用户通过身份验证时显示添加游戏按钮,以及欢迎消息和注销按钮。 但是,当用户未登录时,请显示按钮登录和登录。

Let's have another look at the UI:

让我们再看一下UI:

  1. User Authenticated

    用户认证
  2. User not Authenticated

    用户未认证

Let's create another component to achieve this, in /client/src/components create AddGamePanel.jsx and paste the following code:

让我们创建另一个组件来实现此目的,在/client/src/components创建AddGamePanel.jsx并粘贴以下代码:

import React, {
PureComponent } from 'react';import {
Link } from 'react-router';/* * Import the utility function */ import userAuthenticated from '../utils/authWrapper';class AddGamePanel extends PureComponent {
render () {
/* * userName comes from the state while logout * is an action creator we are going to define later */ const {
userName, logout } = this.props; return (
Welcome back {
userName}, Logout
add a new Game!
); }}/* * Auth-wrapper options */ const options = {
authSelector: state => state.get('auth'), predicate: auth => auth.get('isAuthenticated'), wrapperDisplayName: 'authAddGame', /* * This time the failure component are the buttons * to authenticate the user or register a new one */ FailureComponent: () => {
return (
Sign Up
Login
); }};// We export it export default userAuthenticated(options)(AddGamePanel);

The options are totally the same as the delete buttons ones but this time we render FailureComponent when the user is not authenticated.

这些选项与删除按钮的选项完全相同,但是这一次,我们在未通过用户身份验证时呈现FailureComponent

Now, let's import it inside GamesListManager, edit /client/src/components/GamesListManager.jsx:

现在,让我们将其导入GamesListManager内 ,编辑/client/src/components/GamesListManager.jsx

import React, {
PureComponent } from 'react';import {
Link } from 'react-router';import Game from './Game';/* * Import the new component */import AddGamePanel from './AddGamePanel';export default class GamesListManager extends PureComponent {
render () {
const {
games, searchBar, setSearchBar, toggleModal, deleteGame, userName, /* * The new action-creator to be defined */ logout } = this.props; return (
{
/* * we add the component */ }
{
games .filter(game => game.name.toLowerCase().includes(searchBar)) .map((game, i) => {
return (
); }) }

); }}

Nothing exotic, we just render the component inside its parent GamesListManager.

没什么奇怪的 ,我们只是在其父GamesListManager中渲染该组件。

The userName and logout props come from the GamesContainer so let's edit /client/src/containers/GamesContainer.jsx:

userNamelogout道具来自GamesContainer,所以让我们编辑/client/src/containers/GamesContainer.jsx

import React, {
PureComponent } from 'react';import {
connect } from 'react-redux';import {
bindActionCreators } from 'redux';import Immutable from 'immutable';import {
Modal, GamesListManager } from '../components';import * as gamesActionCreators from '../actions/games';import * as authActionCreators from '../actions/auth';/* * Add toastr to show notifcations */ import {
toastr } from 'react-redux-toastr';class GamesContainer extends PureComponent {
constructor (props) {
super(props); this.toggleModal = this.toggleModal.bind(this); this.deleteGame = this.deleteGame.bind(this); this.setSearchBar = this.setSearchBar.bind(this); // Bind logout to this this.logout = this.logout.bind(this); } componentDidMount () {
this.getGames(); } toggleModal (index) {
this.props.gamesActions.showSelectedGame(this.props.games[index]); $('#game-modal').modal(); } getGames () {
this.props.gamesActions.getGames(); } deleteGame (id) {
this.props.gamesActions.deleteGame(id); } setSearchBar (event) {
this.props.gamesActions.setSearchBar(event.target.value.toLowerCase()); } /* * The function calls an action to remove the user * from the state, show a notification and delete the token from the local storage */ logout () {
this.props.authActions.logoutUser(); toastr.success('Retrogames archive', 'Your are now logged out'); localStorage.removeItem('token'); } render () {
const {
games, selectedGame, searchBar, userName, authActions } = this.props; return (
); }}function mapStateToProps (state) {
return {
games: state.getIn(['games', 'list'], Immutable.List()).toJS(), searchBar: state.getIn(['games', 'searchBar'], ''), selectedGame: state.getIn(['games', 'selectedGame'], Immutable.List()).toJS(), /* * The name comes from token after being decoded */ userName: state.getIn(['auth', 'name']) }}function mapDispatchToProps (dispatch) {
return {
gamesActions: bindActionCreators(gamesActionCreators, dispatch), authActions: bindActionCreators(authActionCreators, dispatch) };}export default connect(mapStateToProps, mapDispatchToProps)(GamesContainer);

As the container contains the logic it's a connected component, this is where we can get the props for AddGamePanel.

由于容器包含逻辑,它是一个连接的组件,因此可以在此获取AddGamePanel的道具。

To conclude, let's create the logout action-creators in /client/src/actions/auth.js:

最后,让我们在/client/src/actions/auth.js创建注销操作创建者:

import {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, /* * Import a new constant */ LOGOUT_USER, SIGNUP_USER, SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE} from '../constants/auth';function loginUser (redirection) {
return {
type: LOGIN_USER, redirection };}function loginUserSuccess (token) {
return {
type: LOGIN_USER_SUCCESS, token };}function loginUserFailure () {
return {
type: LOGIN_USER_FAILURE };}/* * The action-creator */ function logoutUser () {
return {
type: LOGOUT_USER };}function signupUser () {
return {
type: SIGNUP_USER };}function signupUserSuccess (token) {
return {
type: SIGNUP_USER_SUCCESS, token };}function signupUserFailure () {
return {
type: SIGNUP_USER_FAILURE };}export {
loginUser, loginUserSuccess, loginUserFailure, logoutUser, signupUser, signupUserSuccess, signupUserFailure};

And then add the constant in /client/src/constants/auth.js:

然后在/client/src/constants/auth.js添加常量:

const LOGIN_USER = 'LOGIN_USER';const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE';/*  * New constant */ const LOGOUT_USER = 'LOGOUT_USER';const SIGNUP_USER = 'SIGNUP_USER';const SIGNUP_USER_SUCCESS = 'SIGNUP_USER_SUCCESS';const SIGNUP_USER_FAILURE = 'SIGNUP_USER_FAILURE';export {
LOGIN_USER, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, LOGOUT_USER, SIGNUP_USER, SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE};

Finally, the auth reducer is going to intercept the action LOGOUT_USER, edit /client/src/reducers/auth:

最后,auth reducer将截获LOGOUT_USER动作,编辑/client/src/reducers/auth

import Immutable from 'immutable';import jwtDecode from 'jwt-decode';import {
LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, /* * Import the logout constant */ LOGOUT_USER, SIGNUP_USER_SUCCESS, SIGNUP_USER_FAILURE} from '../constants/auth';const initialState = Immutable.Map({
isAuthenticated: false, token: null, name: null});export default (state = initialState, action) => {
switch (action.type) {
case SIGNUP_USER_SUCCESS: case LOGIN_USER_SUCCESS: {
return state.merge({
isAuthenticated: true, token: action.token, name: jwtDecode(action.token).sub }); } case SIGNUP_USER_FAILURE: case LOGIN_USER_FAILURE: // initial state is naturally the logout state case LOGOUT_USER: return state.merge(initialState); default: return state; }}

And that's all, we added authentication to our Archive app!

就是这样,我们向“存档”应用程序添加了身份验证!

There is only one last thing we can do: Whenever a user refreshes the page we should first look for the token in the local storage, in case it's valid we can directly authenticate him/her. createStore function receives an object parameter to define an initial state so we can take advantage of it: A function isAuthenticated looks for the token from the local storage and if the token exists the initial state sends the store the user data during its creation. Create authentication.js in client/src/utils and paste the following code:

我们只能做的最后一件事:每当用户刷新页面时,我们都应该首先在本地存储中查找令牌,如果有效,我们可以直接对其进行身份验证。 createStore函数接收一个对象参数来定义初始状态,因此我们可以利用它: isAuthenticated函数从本地存储中查找令牌,如果令牌存在,则初始状态会在创建过程中向存储发送用户数据。 在client/src/utils创建authentication.js并粘贴以下代码:

import jwtDecode from 'jwt-decode';const isAuthenticated = () => {
const token = localStorage.getItem('token'); // Default initialState let initialState = {
isAuthenticated: false, token: null, name: null }; if (token) {
// This is the same result as LOGIN_USER_SUCCESS initialState = {
isAuthenticated: true, token: token, name: jwtDecode(token).sub }; } return initialState;};export default isAuthenticated;

And finally we can edit /client/src/store.js:

最后,我们可以编辑/client/src/store.js

import {
createStore, applyMiddleware, compose} from 'redux';/* * Wrap initialState in an immutable data-structure */import Immutable from 'immutable';import createSagaMiddleware from 'redux-saga';import rootSaga from './sagas';import reducer from './reducers';import {
routerMiddleware } from 'react-router-redux';import {
hashHistory } from 'react-router';/* * Import isAuthenticated to define initialState */import isAuthenticated from './utils/authentication';/* * We define the initialState with only information regarding the authentication */ const initialState = Immutable.fromJS({
auth: isAuthenticated()});const configureStore = () => {
const sagaMiddleware = createSagaMiddleware(); const routeMiddleware = routerMiddleware(hashHistory); const store = createStore( reducer, initialState, // We pass the initialState for user authentication compose( applyMiddleware(sagaMiddleware, routeMiddleware), window.devToolsExtension ? window.devToolsExtension() : (f) => f ) ); sagaMiddleware.run(rootSaga); return store;}export default configureStore;

And this is the end of the tutorial, Congratulations!

恭喜,这就是本教程的结尾!

( )

In this part.3 of the tutorial to create a Retrogame Archive app we added a simple authentication logic to limit the actions users can do when not logged in: They can only view the games, to modify the database they must be authorized.

在本教程的第3部分中,创建了Retrogame Archive应用程序,我们添加了一个简单的身份验证逻辑,以限制用户未登录时可以执行的操作:他们只能查看游戏,修改必须经过授权的数据库。

To do so we first created two useful roues on our Node.js server to handle signup and login. Both returns a token that it's exchanged whenever the user wants to add or delete a game. To retrieve the token from HTTP requests we wrote a middleware.

为此,我们首先在Node.js服务器上创建了两个有用的路由来处理注册和登录。 当用户想要添加或删除游戏时,两者都返回交换令牌。 为了从HTTP请求中检索令牌,我们编写了一个中间件。

In the client-side we added redux-auth-wrapper which helps us to write high order components to wrap routes or components. By doing so we can show/hide buttons and protect views from unauthorized users.

在客户端,我们添加了redux-auth-wrapper ,它帮助我们编写高阶组件来包装路由或组件。 这样,我们可以显示/隐藏按钮并防止未经授权的用户查看。

( )

It was definitely a long tutorial, divided in 3 parts, I hope you enjoyed it as I did while writing this fun app. However there are tons of optimization that can be done, first of all a general refactoring of the components: To name one, there may not be need separate components for signup and login views.

这绝对是一个漫长的教程,分为三个部分,希望您像我编写这个有趣的应用程序一样喜欢它。 但是,可以进行大量的优化工作,首先对组件进行一般的重构:举一个例子,注册和登录视图可能不需要单独的组件。

Here I summarizied a few suggestions:

在这里,我总结了一些建议:

  1. Writing constants and action-creators can become tedious (reducers included) so I suggest you to read the redux to find strategies to reduce boilerplate.

    编写常量和动作创建者可能会变得乏味(包括缩减器),因此我建议您阅读redux 以找到减少样板的策略。
  2. The same can be said for Redux-saga, take a look at the and you should get some hints on how to refactor the sagas we wrote.

    对于Redux-saga来说也可以这么说,看看 ,您应该获得关于如何重构我们编写的sagas的一些提示。
  3. Finally, the webpack configuration to serve the bundle from Node.js is not optimized for production, which is why I set the NODE_ENV=build, not production. Also, Webpack 2 is at the time I wrote this tutorial very stable so I would suggest you all to start using it, the to migrate from v1 to v2 is very complete.

    最后,用于Node.js捆绑软件的webpack配置并未针对生产进行优化,这就是为什么我设置NODE_ENV = build而非生产的原因。 另外,在我编写本教程时,Webpack 2非常稳定,因此建议大家开始使用它,从v1迁移到v2的非常完整。

翻译自:

转载地址:http://geuwd.baihongyu.com/

你可能感兴趣的文章
数据库三大范式
查看>>
工作总结之二:bug级别、优先级别、bug状态
查看>>
访问修饰符、封装、继承
查看>>
更换pip源到国内镜像,提升pip下载速度.
查看>>
POJ 2265 Bee Maja (找规律)
查看>>
Kendo MVVM 数据绑定(七) Invisible/Visible
查看>>
[zz]kvm环境使用libvirt创建虚拟机
查看>>
bzoj1059 [ZJOI2007]矩阵游戏
查看>>
插入返回ibatis 的selectKey 实现插入数据后获得id
查看>>
vim 程序编辑器
查看>>
LIS(单调队列优化 C++ 版)(施工ing)
查看>>
刚接触Vuex
查看>>
四种加载React数据的技术对比(Meteor 转)
查看>>
Airthmetic_Approching
查看>>
操作文本文件
查看>>
公司项目的几个问题
查看>>
解决win7下打开Excel2007,报“向程序发送命令时出现问题”的错误
查看>>
Velocity快速入门教程
查看>>
关于集合常见的问题
查看>>
车牌正则表达式
查看>>